#!/bin/bash

# Copyright 2024 Balena Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


# This script hooks to the "up" event of every interface
# It checks whether there is a rule commented "nm-shared-$IFNAME"
# in the FORWARD chain and if it is, removes it from its current
# position and appends to the end.
#
# This is necessary because of a race condition between NetworkManager
# and the balenaEngine when setting up their respective sets of firewall
# rules. If the balenaEngine sets up its rules last (IOW on top
# of the FORWARD chain), containers are allowed to route to the DHCP
# clients behind the shared interface. If NetworkManager sets its rules
# last, the containers will be denied access to the shared network.
#
# While the latter seems cleaner, some users actually benefit from
# the containers being able to route behind the shared interface,
# so we choose to prefer that behavior. This script overcomes
# the race condition and always sets the FORWARD rules
# as if balenaEngine came up last.

# shellcheck disable=SC1091
. /usr/libexec/os-helpers-logging

if [ "$2" != "up" ]
then
  exit 0
fi

IFNAME="$1"

# Use -w 5 to wait for the lock file if necessary
# 5 seconds is ridiculously high but this is just a sanity limit
# to prevent it from hanging infinitely if nothing removes the lock
IPTABLES="iptables -w 5"

# Look for the FORWARD rule that NetworkManager adds for interfaces
# configured as shared. This will have a comment "nm-shared-$IFNAME"
# and jump into a chain named "sh-fw-$IFNAME"
FW_RULE_COMMENT="nm-shared-${IFNAME}"
FW_RULE_ARGS=$(${IPTABLES} -S FORWARD | grep "sh-fw-${IFNAME}" | grep "${FW_RULE_COMMENT}")
if [ -z "${FW_RULE_ARGS}" ]
then
  exit 0
fi

# Sometimes on NetworkManager restart a new rule is added
# but the old one is not properly cleand up
# Remove the duplicates here as the rules are all the same
DUPS=0
while [ "$(echo "${FW_RULE_ARGS}" | wc -l)" -gt 1 ]
do
  DUPS=$(("${DUPS}" + 1))
  FIRST_FW_RULE_ARGS="$(echo "${FW_RULE_ARGS}" | head -n 1)"
  ${IPTABLES} -D ${FIRST_FW_RULE_ARGS#-A }
  FW_RULE_ARGS=$(${IPTABLES} -S FORWARD | grep "sh-fw-${IFNAME}" | grep "${FW_RULE_COMMENT}")
done

if [ "${DUPS}" -gt 0 ]
then
  info "Removed ${DUPS} duplicate '${FW_RULE_COMMENT}' rules"
fi

# If the rule is already last, this will do nothing
# If the rule is not last, the first run through the loop should move it
# If that does not work for any reason, try a few more times before bailing out
I=0
while [ "$(${IPTABLES} -S FORWARD | tail -n 1)" != "${FW_RULE_ARGS}" ]
do
  I=$(("${I}" + 1))

  if [ "${I}" -gt 5 ]
  then
    fail "5 attempts to move the '${FW_RULE_COMMENT}' firewall rule to the last position have failed, bailing out"
  fi

  # Append the rule to the bottom
  # Do not quote ${FW_RULE_ARGS}, this needs to expand
  ${IPTABLES} ${FW_RULE_ARGS}

  # Remove the rule from its original position
  ${IPTABLES} -D ${FW_RULE_ARGS#-A }
done

if [ "${I}" = 0 ]
then
  info "Rule '${FW_RULE_COMMENT}' was already in the last position"
else
  info "Moved the '${FW_RULE_COMMENT}' rule to the last position"
  if [ "${I}" -gt 1 ]
  then
    warn "It took ${I} attempts to move the rule"
  fi
fi
